package org.sbbs.mojo.exporter;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.Collection;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.plexus.components.interactivity.Prompter;
import org.codehaus.plexus.components.interactivity.PrompterException;
import org.hibernate.tool.hbm2x.Exporter;
import org.sbbs.mojo.HibernateExporterMojo;
import org.sbbs.tool.ArtifactInstaller;
import org.sbbs.tool.SbbsAbstractExporter;
import org.sbbs.tool.SingleEntityExporter;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Generates Java classes from set of annotated POJOs. Use -DdisableInstallation
 * to prevent installation. If using this goal in a "core" module or project,
 * only DAOs and Managers will be created. For "web" modules, the same principle
 * applies.
 * 
 * @author <a href="mailto:matt@raibledesigns.com">Matt Raible</a>
 * @goal gen
 * @phase generate-sources
 * @execute phase="compile"
 */
public class SbbsGeneratorMojo extends HibernateExporterMojo {

	boolean generateCoreOnly;
	boolean generateWebOnly;

	String functionType;
	String masterPojoName = null;
	String detailPojoName = null;
	String pojoName;

	/**
	 * This is a prompter that can be user within the maven framework.
	 * 
	 * @component
	 */
	Prompter prompter;

	/**
	 * The path where the generated artifacts will be placed. This is
	 * intentionally not set to the default location for maven generated
	 * sources. This is to keep these files out of the eclipse/idea generated
	 * sources directory as the intention is that these files will be copied to
	 * a source directory to be edited and modified and not re generated each
	 * time the plugin is run. If you want to regenerate the files each time you
	 * build the project just set this value to
	 * ${basedir}/target/generated-sources or set the flag on eclipse/idea
	 * plugin to include this file in your project file as a source directory.
	 * 
	 * @parameter expression="${sbbs.destinationDirectory}"
	 *            default-value="${basedir}"
	 * @noinspection UnusedDeclaration
	 */
	private String destinationDirectory;

	/**
	 * The directory containing the source code.
	 * 
	 * @parameter expression="${sbbs.sourceDirectory}"
	 *            default-value="${basedir}/target/sbbs/generated-sources"
	 * @noinspection UnusedDeclaration
	 */
	private String sourceDirectory;

	/**
	 * Allows disabling installation - for tests and end users that don't want
	 * to do a full installation
	 * 
	 * @parameter expression="${sbbs.disableInstallation}" default-value="false"
	 */
	private boolean disableInstallation;

	/**
	 * @parameter expression="${sbbs.genericCore}" default-value="true"
	 * @noinspection UnusedDeclaration
	 */
	private boolean genericCore;

	/**
	 * @parameter expression="${sbbs.templateDirectory}"
	 *            default-value="${basedir}/src/test/resources"
	 * @noinspection UnusedDeclaration
	 */
	private String templateDirectory;

	/**
	 * Path for where to generate files at.
	 */

	private String fullPath = null;

	/**
	 * Default constructor.
	 */
	public SbbsGeneratorMojo() {
		addDefaultComponent("target/sbbs/generated-sources", "configuration", false);
		addDefaultComponent("target/sbbs/generated-sources", "annotationconfiguration", true);
	}

	// --------------------- Interface ExporterMojo ---------------------

	/**
	 * Returns <b>gen</b>.
	 * 
	 * @return String goal's name
	 */
	public String getName() {
		return "gen";
	}

	@Override
	public void execute() throws MojoExecutionException, MojoFailureException {
		// if project is of type "pom", throw an error
		if (getProject().getPackaging().equalsIgnoreCase("pom")) {
			String errorMsg = "[ERROR] This plugin cannot be run from a pom project, please run it from a jar or war project (i.e. core or web).";
			// getLog().error(errorMsg);
			throw new MojoFailureException(errorMsg);
		}

		functionType = System.getProperty("ftype");
		if (functionType == null) {
			try {
				functionType = prompter.prompt("please select your function type ,single entity:1 master detail:2?",
						"1");
			} catch (PrompterException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		if (functionType.equalsIgnoreCase("1")) {
			if (functionType == null || "".equals(functionType.trim())) {
				throw new MojoExecutionException("You must specify an entity name to continue.");
			}

			pojoName = System.getProperty("entity");

			if (pojoName == null) {
				try {
					pojoName = prompter.prompt("What is the name of your pojo (i.e. Person)?");
				} catch (PrompterException pe) {
					pe.printStackTrace();
				}
			}
			genExec();
		} else if (functionType.equalsIgnoreCase("2")) {

			try {
				masterPojoName = prompter
						.prompt("please input master pojo Name with full package path,and the entity java file in model package,like org.sbbs.app.model.DemoEntity:");
				detailPojoName = prompter
						.prompt("please input detail pojo Name with full package path,and the entity java file in model package,like org.sbbs.app.model.DemoEntity:");

			} catch (PrompterException e) {
				e.printStackTrace();
			}
			pojoName = masterPojoName;
			this.processPojoName();
			addEntityToHibernateCfgXml();
			pojoName = detailPojoName;
			this.processPojoName();
			addEntityToHibernateCfgXml();

			pojoName = masterPojoName;
			this.processPojoName();
			super.execute();
			pojoName = detailPojoName;
			this.processPojoName();
			super.execute();

		} else {
			String errorMsg = "[ERROR] You should enter currect function type number 1 or 2, but your entery is: "
					+ functionType;
			throw new MojoFailureException(errorMsg);
		}

	}

	private void genExec() throws MojoExecutionException, MojoFailureException {
		this.processPojoName();
		addEntityToHibernateCfgXml();
		super.execute();
	}

	private void addEntityToHibernateCfgXml() throws MojoFailureException {
		String daoFramework = getProject().getProperties().getProperty("dao.framework");

		if (getComponentProperty("configurationfile") == null) {
			getComponentProperties().put("configurationfile", "src/main/resources/hibernate.cfg.xml");
		}

		// if entity is not in hibernate.cfg.xml, add it
		String hibernateConfig = getComponentProperty("configurationfile");

		if (daoFramework.equals("hibernate")) {
			try {
				String hibernateCfgXml = FileUtils.readFileToString(new File(hibernateConfig));
				addEntityToHibernateCfgXml(hibernateCfgXml);
			} catch (IOException io) {
				throw new MojoFailureException(io.getMessage());
			}
		}
	}

	private void processPojoName() throws MojoExecutionException, MojoFailureException {
		if (pojoName == null || "".equals(pojoName.trim())) {
			throw new MojoExecutionException("You must specify an entity name to continue.");
		}

		// A dot in the entity name means the person is specifying the
		// package.
		if (pojoName.contains(".")) {
			if (pojoName.indexOf("model") == -1) {
				throw new MojoExecutionException("You must specify 'model' as the last package in your entity name.");
			}
			System.setProperty("entity", pojoName);
			fullPath = pojoName.substring(0, pojoName.indexOf(".model"));
			// allow ~ to be used for groupId
			fullPath = fullPath.replace("~", getProject().getGroupId());

			pojoName = pojoName.substring(pojoName.lastIndexOf(".") + 1);
			log("Package name set to: " + fullPath);
		}

	}

	/**
	 * @see org.sbbs.mojo.HibernateExporterMojo#configureExporter(org.hibernate.tool.hbm2x.Exporter)
	 */
	protected Exporter configureExporter(Exporter exp) throws MojoExecutionException {
		// Read in SbbsAbstractExporter#configureExporter to decide if a class
		// should be
		// generated or not
		System.setProperty("sbbs.entity", pojoName);

		// add output directory to compile roots
		getProject().addCompileSourceRoot(new File(getComponent().getOutputDirectory()).getPath());

		// now set the extra properties for the SbbsAbstractExporter
		SbbsAbstractExporter exporter = (SbbsAbstractExporter) super.configureExporter(exp);
		exporter.getProperties().setProperty("ejb3", getComponentProperty("ejb3", "true"));
		exporter.getProperties().setProperty("jdk5", getComponentProperty("jdk5", "true"));

		if (generateCoreOnly) {
			exporter.getProperties().setProperty("generate-core", "true");
		} else if (generateWebOnly) {
			exporter.getProperties().setProperty("generate-web", "true");
		}

		String rootPackage = (fullPath != null) ? fullPath : getProject().getGroupId();

		// Sbbs-specific values
		exporter.getProperties().setProperty("basepackage", rootPackage);
		exporter.getProperties().setProperty("daoframework", getProject().getProperties().getProperty("dao.framework"));

		String webFramework = (getProject().getProperties().containsKey("web.framework")) ? getProject()
				.getProperties().getProperty("web.framework") : "";

		exporter.getProperties().setProperty("webframework", webFramework);

		exporter.getProperties().setProperty("packaging", getProject().getPackaging());
		exporter.getProperties().setProperty("genericcore", String.valueOf(genericCore));

		if (templateDirectory != null) {
			exporter.getProperties().setProperty("templatedirectory", templateDirectory);
		}

		if (isFullSource())
			exporter.getProperties().setProperty("sbbspackage", rootPackage);
		else {
			exporter.getProperties().setProperty("sbbspackage", "org.sbbs");
		}

		// See if the project has security enabled
		boolean hasSecurity = false;
		if (getProject().getPackaging().equals("war")) {
			Collection<File> sourceFiles = FileUtils.listFiles(getProject().getBasedir(), new String[] { "xml" }, true);
			for (File file : sourceFiles) {
				if (file.getPath().contains("security.xml")) {
					hasSecurity = true;
					break;
				}
			}
		}

		exporter.getProperties().setProperty("hasSecurity", String.valueOf(hasSecurity));

		// determine if using Home or MainMenu for Tapestry
		if (webFramework.equals("tapestry")) {
			boolean useMainMenu = true;
			Collection<File> sourceFiles = FileUtils
					.listFiles(getProject().getBasedir(), new String[] { "java" }, true);
			for (File file : sourceFiles) {
				if (file.getPath().contains("Home.java")) {
					useMainMenu = false;
					break;
				}
			}
			exporter.getProperties().setProperty("useMainMenu", String.valueOf(useMainMenu));
		}

		return exporter;
	}

	/**
	 * Executes the plugin in an isolated classloader.
	 * 
	 * @throws MojoExecutionException
	 *             When there is an erro executing the plugin
	 */
	@Override
	protected void doExecute() throws MojoExecutionException {
		super.doExecute();

		if (System.getProperty("disableInstallation") != null) {
			disableInstallation = Boolean.valueOf(System.getProperty("disableInstallation"));
		}

		// allow installation to be suppressed when testing
		if (!disableInstallation) {
			ArtifactInstaller installer = new ArtifactInstaller(getProject(), pojoName, sourceDirectory,
					destinationDirectory, genericCore);
			installer.execute();
		}
	}

	/**
	 * Instantiates a org.sbbs.tool.SbbsAbstractExporter object.
	 * 
	 * @return POJOExporter
	 */
	protected Exporter createExporter() {
		// return new SbbsAbstractExporter();
		return new SingleEntityExporter();
	}

	protected void setGenerateCoreOnly(boolean generateCoreOnly) {
		this.generateCoreOnly = generateCoreOnly;
	}

	protected void setGenerateWebOnly(boolean generateWebOnly) {
		this.generateWebOnly = generateWebOnly;
	}

	private void log(String msg) {
		getLog().info("[Sbbs] " + msg);
	}

	private void addEntityToHibernateCfgXml(String hibernateCfgXml) throws MojoFailureException {
		String className = (fullPath != null) ? fullPath : getProject().getGroupId();

		className += ".model." + pojoName;
		log("Constructed class name : " + className);

		POJOSearcher pojoSearcher = new POJOSearcher(hibernateCfgXml);
		if (!pojoSearcher.searchForPojo(pojoName)) {
			// check that class exists and has an @Entity annotation
			checkEntityExists();

			hibernateCfgXml = hibernateCfgXml.replace("</session-factory>", "    <mapping class=\"" + className
					+ "\"/>" + "\n    </session-factory>");
			log("Adding '" + pojoName + "' to hibernate.cfg.xml...");
		}

		hibernateCfgXml = hibernateCfgXml.replaceAll("\\$\\{sbbspackage}", (isFullSource()) ? getProject().getGroupId()
				: "org.sbbs");

		try {
			FileUtils.writeStringToFile(
					new File(getComponentProperty("configurationfile", "src/main/resources/hibernate.cfg.xml")),
					hibernateCfgXml);
		} catch (IOException io) {
			throw new MojoFailureException(io.getMessage());
		}
	}

	private void checkEntityExists() throws MojoFailureException {
		// allow check to be bypassed when -Dentity.check=false
		if (!"false".equals(System.getProperty("entity.check"))) {
			final String FILE_SEP = System.getProperty("file.separator");
			String pathToModelPackage = "src" + FILE_SEP + "main" + FILE_SEP + "java" + FILE_SEP;
			if (getProject().getPackaging().equals("war") && getProject().hasParent()) {
				String moduleName = (String) getProject().getParent().getModules().get(0);
				String pathToParent = getProject().getOriginalModel().getParent().getRelativePath();
				pathToParent = pathToParent.substring(0, pathToParent.lastIndexOf('/') + 1);
				pathToParent = pathToParent.replaceAll("/", FILE_SEP);
				pathToModelPackage = getProject().getBasedir() + FILE_SEP + pathToParent + moduleName + "/"
						+ pathToModelPackage;
			}

			// refactor to check classpath instead of filesystem
			String groupIdAsPath = getProject().getGroupId().replace(".", FILE_SEP);

			String fullPathPackage = pathToModelPackage + groupIdAsPath;
			if (fullPath != null) {
				fullPathPackage += FILE_SEP + fullPath.replace(".", FILE_SEP);
			} else {
				fullPathPackage += FILE_SEP + "model";
			}
			log("Looking for entity in " + fullPathPackage);

			File modelPackage = new File(fullPathPackage);
			boolean entityExists = false;

			if (modelPackage.exists()) {
				String[] entities = modelPackage.list();
				for (String entity : entities) {
					log("Found '" + entity + "' in model package...");
					if (entity.contains(pojoName + ".java")) {
						entityExists = true;
						break;
					}
				}
			} else {
				getLog().error("The path '" + fullPathPackage + groupIdAsPath + FILE_SEP + "model' does not exist!");
			}

			if (!entityExists) {
				throw new MojoFailureException("[ERROR] The '" + pojoName + "' entity does not exist in '"
						+ modelPackage + "'.");
			} else {
				// Entity found, make sure it has @Entity annotation
				try {
					File pojoFile = new File(modelPackage + FILE_SEP + pojoName + ".java");
					String entityAsString = FileUtils.readFileToString(pojoFile);
					if (!entityAsString.contains("@Entity")) {
						String msg = "Entity '" + pojoName
								+ "' found, but it doesn't contain an @Entity annotation. Please add one.";
						throw new MojoFailureException(msg);
					}
				} catch (IOException io) {
					throw new MojoFailureException("[ERROR] Class '" + pojoName + ".java' not found in '"
							+ modelPackage + "'");
				}
			}
		}
	}

	public class POJOSearcher extends DefaultHandler {
		private String pojoName;
		private boolean foundPojo;
		private String xmlString;

		public POJOSearcher(String xmlString) {
			this.xmlString = xmlString;
		}

		public boolean searchForPojo(String pojoName) {
			this.pojoName = pojoName;
			this.foundPojo = false;

			SAXParserFactory spf = SAXParserFactory.newInstance();
			try {
				spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);

				SAXParser sp = spf.newSAXParser();
				sp.parse(new InputSource(new StringReader(xmlString)), this);
			} catch (Exception ex) {
				System.out.println(ex.getMessage());
				ex.printStackTrace();
			}

			return foundPojo;
		}

		@Override
		public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
			super.startElement(uri, localName, name, attributes);
			if (name.equals("mapping")) {
				String classValue = attributes.getValue("class");
				if (classValue != null) {
					if (classValue.endsWith(pojoName)) {
						foundPojo = true;
					}
				}
			}
		}
	}
}
